package interpreter;

import interpreter.builtin.functions.ExactPredicate;
import interpreter.builtin.functions.InexactPredicate;
import interpreter.builtin.functions.IntegerPredicate;
import interpreter.builtin.functions.MinusFunction;
import interpreter.builtin.functions.NumberEqualPredicate;
import interpreter.builtin.functions.NumberPredicate;
import interpreter.builtin.functions.PlusFunction;
import interpreter.builtin.functions.RealPredicate;

import java.util.ArrayList;
import java.util.List;

import parsing.Parser;
import ast.Abbreviation;
import ast.Assignment;
import ast.Body;
import ast.BooleanConstant;
import ast.CharacterConstant;
import ast.Condition;
import ast.Definition;
import ast.ExactComplex;
import ast.Expression;
import ast.FunctionCall;
import ast.FunctionDefinition;
import ast.InexactComplex;
import ast.Lambda;
import ast.ListDatum;
import ast.Node;
import ast.Quote;
import ast.StringConstant;
import ast.Variable;
import ast.VariableDefinition;
import ast.VectorDatum;
import ast.Visitor;

public class Interpreter implements Visitor {

	private Parser parser;
	private Object result;
	private SymbolTable symbolTable = new SymbolTable();

	public Interpreter(Parser parser) {
		this.parser = parser;
		addBuiltInFunctionsToGlobalScope();
	}

	private void addBuiltInFunctionsToGlobalScope() {
		addArithmeticOperatorsToGlobalScope();
		addNumberPredicatesToGlobalScope();
	}

	private void addArithmeticOperatorsToGlobalScope() {
		symbolTable.put(new Symbol("+", new PlusFunction()));
		symbolTable.put(new Symbol("-", new MinusFunction()));
	}

	private void addNumberPredicatesToGlobalScope() {
		symbolTable.put(new Symbol("=", new NumberEqualPredicate()));
		symbolTable.put(new Symbol("number?", new NumberPredicate()));
		symbolTable.put(new Symbol("complex?", new NumberPredicate()));
		symbolTable.put(new Symbol("real?", new RealPredicate()));
		symbolTable.put(new Symbol("rational?", new RealPredicate()));
		symbolTable.put(new Symbol("integer?", new IntegerPredicate()));
		symbolTable.put(new Symbol("exact?", new ExactPredicate()));
		symbolTable.put(new Symbol("inexact?", new InexactPredicate()));
	}

	public boolean hasNextForm() {
		return parser.hasNextForm();
	}

	public Object evaluateNextForm() {
		parser.parseNextForm().accept(this);
		return result;
	}

	public Object evaluate(Node expression) {
		expression.accept(this);
		return result;
	}

	@Override
	public void visitAbbreviation(Abbreviation abbreviation) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visitAssignment(Assignment assignment) {
		assignment.getExpression().accept(this);
		try {
			symbolTable.set(assignment.getVariable(), result);
			result = null;
		} catch (SymbolTable.UnknownVariableCannotBeSet e) {
			throw new UnknownVariableCannotBeSet(assignment.getVariable());
		}
	}

	@SuppressWarnings("serial")
	public class UnknownVariableCannotBeSet extends RuntimeException {
		private String variable;

		public UnknownVariableCannotBeSet(String variable) {
			this.variable = variable;
		}

		public String getVariable() {
			return variable;
		}
	}

	@Override
	public void visitBody(Body body) {
		for (Definition d : body.getDefinitions())
			d.accept(this);
		for (Expression e : body.getExpressions())
			e.accept(this);
	}

	@Override
	public void visitBooleanConstant(BooleanConstant booleanConstant) {
		result = booleanConstant.getValue();
	}

	@Override
	public void visitCharacterConstant(CharacterConstant characterConstant) {
		result = characterConstant.getValue();
	}

	@Override
	public void visitCondition(Condition condition) {
		if (isFalse(condition.getCondition()))
			evaluateIfNotNull(condition.getElseClause());
		else
			condition.getIfClause().accept(this);
	}

	private void evaluateIfNotNull(Expression e) {
		if (e == null)
			result = null;
		else
			e.accept(this);
	}

	private boolean isFalse(Expression expression) {
		expression.accept(this);
		return result instanceof Boolean && ((Boolean) result) == false;
	}

	@Override
	public void visitExactComplex(ExactComplex exactComplex) {
		result = exactComplex.getValue();
	}

	@Override
	public void visitFunctionCall(FunctionCall functionCall) {
		functionCall.getCallee().accept(this);
		Callable c = (Callable) result;
		List<Object> arguments = new ArrayList<>();
		for (Expression arg : functionCall.getArguments())
			arguments.add(evaluate(arg));
		result = c.call(arguments);
	}

	@Override
	public void visitFunctionDefinition(FunctionDefinition functionDefinition) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visitInexactComplex(InexactComplex inexactComplex) {
		result = new math.inexact.Complex(inexactComplex.getReal(),
				inexactComplex.getImag());
	}

	@Override
	public void visitLambda(Lambda lambda) {
		result = new LambdaCallable(lambda, this, symbolTable);
	}

	@Override
	public void visitListDatum(ListDatum listDatum) {
		List<Object> data = new ArrayList<>();
		for (Expression datum : listDatum.getData())
			data.add(evaluate(datum));
		result = createList(data);
	}

	private Object createList(List<Object> data) {
		if (data.size() == 0)
			return EmptyList.instance;
		return new SchemePair(data.get(0), createList(data.subList(1,
				data.size())));
	}

	@Override
	public void visitQuote(Quote quote) {
		quote.getDatum().accept(this);
	}

	@Override
	public void visitStringConstant(StringConstant stringConstant) {
		result = stringConstant.getValue();
	}

	@Override
	public void visitVariable(Variable variable) {
		result = symbolTable.get(variable.getIdentifier()).getValue();
	}

	@Override
	public void visitVariableDefinition(VariableDefinition def) {
		String name = def.getVariable().getIdentifier();
		def.getExpression().accept(this);
		symbolTable.put(new Symbol(name, result));
		result = null;
	}

	@Override
	public void visitVectorDatum(VectorDatum vectorDatum) {
		// TODO Auto-generated method stub

	}

	public SymbolTable getSymbolTable() {
		return symbolTable;
	}

}
